#include "iConfig.hpp"

#include <glf/io/fs.h>
#include <glf/io/archiveManager.h>
#include <glf/io/fileStream.h>
#include <glf/io/crcChecker.h>

#include <glf/core/str.h>

#include <glf/fstl/fixed_vector.h>

#include <string>
#include <sstream>
#include <map>
#include <algorithm>

#include <ctype.h>

#if GLF_COMPILER_SNC
#   pragma diag_push
#   pragma diag_suppress=187
#endif

namespace glf {

// Static init of Fs::ObfuscationMap
namespace
{
static std::map<stringc, stringc> ObfuscationMap;
}

struct FsImplBase : public ios {
	Fs& mFs;

	FsImplBase(Fs& fs) :
		mFs(fs) {
	}

	virtual ~FsImplBase() {
	}
	virtual void init() {
	}
	
	virtual bool MkDir(const char* dir, openflag flag) {
		return false;
	}

	virtual bool RmDir(const char* dir, openflag flag) {
		return false;
	}

	virtual bool EmptyDir(const char* dir, openflag flag) {
		return false;
	}

	virtual bool DoesFileExist(const char* path) {
		return false;
	}

	virtual void GetCwd(int size, char* buffer) {
	}

	virtual void SetCwd(const char* cwd) {

	}

	virtual bool CopyFile(const char* src, openflag srcLoc, const char* dst, openflag dstLoc) {
		FileStream srcStream(src, srcLoc|read);
		FileStream dstStream(dst, dstLoc|write|create|trunc);
		
		if(srcStream.IsOpened() && dstStream.IsOpened()) {
			while(true) {
				char buffer[1024];
				int srcRead = srcStream.ReadTN(buffer);
				if(srcRead > 0) {
					dstStream.Write(buffer, srcRead);
				} else {
					break;
				}
			}
			
			return true;
		}
		return false;
	}
};

}

#if GLF_PLATFORM_IPHONE
	#include "fs_apple_.hpp"
#elif GLF_PLATFORM_MACOSX
	#include "fs_posix_.hpp"
#else
	#include GLFi_HEADER_API(fs, GLF_API_FS)
#endif


namespace glf {

bool IsValidDir(const char* path) {
	return !(Strcmp(path, ".") == 0 || Strcmp(path, "..") == 0);
}

void Fs::_CopyDataToHomeData(fs::DirHandle& dh) {
	
	do {
		stringc buffer = JoinPath(dh.mPath, dh.fileInfo.filename);

		if(dh.fileInfo.isDir) {
			
			MkDir(buffer.c_str(), ios::loc_bak);
			glf::Console::Print("dir: %s\n", buffer.c_str());
			
			fs::DirHandle newHandle;
			if(newHandle.FindFirst(dh, dh.fileInfo.filename.c_str(), dh.mFlag)) {
				_CopyDataToHomeData(newHandle);
			}
			
		} else {
			glf::Console::Print("copy %s from data to home\n", buffer.c_str());
			CopyFile(buffer.c_str(), loc_data, buffer.c_str(), loc_bak);
		}
	} while(dh.FindNext());
}

void Fs::ClearHomeDataDir() {
	RmDir("", loc_bak);
}

void Fs::UpdateHomeDataDir() {
	fs::DirHandle dh;

	MkDir("", loc_bak);
	
	if(dh.FindFirst(".", list_dir|list_file)) {
		_CopyDataToHomeData(dh);
	}
}

void Fs::Win32OverrideDataPath(const char* newPath) {
#if defined(GLF_PLATFORM_WIN32) || defined(GLF_PLATFORM_WP8)
	if(newPath) {
		mDataDir = newPath;
	}
#elif defined(WIN32) && defined(GLF_PLATFORM_JIL)
	if(newPath) {
		mDataDir = newPath;
	}
#elif defined(WIN32) && defined(GLF_PLATFORM_PALMPRE)
	if(newPath) {
		mDataDir = newPath;
	}
#endif
}
	
void Fs::IphoneOverrideDataPath(const char* newPath) {
#if GLF_PLATFORM_IPHONE
	if(newPath) {
		mDataDir = ".";
		chdir(newPath);
	}
#endif
}

	
void Fs::MacOSXOverrideDataPath(const char* newPath) {
#if GLF_PLATFORM_MACOSX
	if(newPath) {
		mDataDir = newPath;
	} 
#endif
}

Fs* Fs::sFs = 0;
	
Fs::Fs()
	: m(GLF_NEW_IMPL(*this))
	, mIsForcingLowercase(false)
{
	GLF_ASSERT(sFs == 0);
	sFs = this;
	//GetGlobals()->fs = this;
	//mDataDir[0] = mHomeDir[0] = mTmpDir[0] = 0;

	mNumLinked = 0;

	for(uint i = 0; i < GLF_ASIZEOF(mLinked); ++i) {
		mLinked[i] = &mLinks[i];
	}

	m.init();
}

Fs::~Fs() {
	sFs = 0;
	GLF_DELETE_IMPL();
}

bool Fs::CopyFile(const char* src, openflag srcLoc, const char* dst, openflag dstLoc) {
	return m.CopyFile(src, srcLoc, dst, dstLoc);
}

const char* Fs::GetDir(openflag loc) const {
	loc = loc & loc_mask;
	
	const char* dir = mDataDir.c_str();
	
	if(loc == loc_data) {
		dir = mDataDir.c_str();
	} else if(loc == loc_home) {
		dir = mHomeDir.c_str();
	} else if(loc == loc_tmp) {
		dir = mTmpDir.c_str();
	} else if(loc == loc_bak) {
		dir = mBakDir.c_str();
	} else if(loc == loc_raw) {
		dir = mRawDir.c_str();
	} else if(loc == loc_cwd) {
		char buffer[2048];
		m.GetCwd(sizeof(buffer), buffer);
		mCwdDir = buffer;
		dir = mCwdDir.c_str();
	} else {
		dir = mDataDir.c_str();
	}
	
	return dir;
}

void Fs::SetDir(openflag loc, const char* newPath) {
	loc = loc & loc_mask;

	if(loc == loc_data) {
		mDataDir = newPath;
	} else if(loc == loc_home) {
		mHomeDir = newPath;
	} else if(loc == loc_tmp) {
		mTmpDir = newPath;
	} else if(loc == loc_bak) {
		mBakDir = newPath;
	} else if(loc == loc_raw) {
		mRawDir = newPath;
	} else if(loc == loc_cwd) {
		m.SetCwd(newPath);
	}
}

void Fs::SetDataDir(const char* newPath) {
	SetDir(ios::loc_data, newPath);
}
void Fs::SetHomeDir(const char* newPath) {
	SetDir(ios::loc_home, newPath);
}
void Fs::SetTempDir(const char* newPath) {
	SetDir(ios::loc_tmp, newPath);
}

const char* Fs::GetDataDir() {
	return GetDir(ios::loc_data);
}

const char* Fs::GetHomeDir() {
	return GetDir(ios::loc_home);
}
const char* Fs::GetTmpDir() {
	return GetDir(ios::loc_tmp);
}

bool Fs::LoadObfuscationMap(const char* filename, unsigned char key)
{
	// Load CrcMaps
	stringc crcfilename = filename;
	crcfilename += ".crc";
	CrcChecker::AddCrcMap(crcfilename.c_str(), false);
	crcfilename = filename;
	crcfilename += ".dyn.crc";
	CrcChecker::AddCrcMap(crcfilename.c_str(), true);

	// Load the map
    FileStream file(filename);
	size_t size = file.GetSize();
	std::vector<char> buf(size+1);
	char* buffer = &buf[0];
	file.Read(buffer, size);
	file.Close();
	buffer[size] = 0;// zero temination of the file (needed for the validation)

	int currentKey = key;
	int chars[3] = { 0, 0, 0 };
	// Decypher the map
	for(unsigned int i = 0; i < size; ++i)
	{
		int character = int(buffer[i]) - currentKey;
		character += (character < 0 ? 256 : 0);
		chars[i % 3] = character;
		
        // Ok, this line looks horrible, but it helps to vary the key in a way that
        // make it hard to accidentally have a match somewhere that would
        // allow the user to decypher part of the file using a wrong key.
		// This uses the last 3 "decyphered" char as base, so havin wrong entry 
		// in the past make it less likly to have a good key now :)
		int keyInc = (character / 2 * (character % 2)) % key * (i % 3 - 1) + ((chars[0] + chars[1] + chars[2]) / 3);
		currentKey = (currentKey + keyInc) % 256;
		currentKey += (currentKey < 0 ? 256 : 0);
		buffer[i] = character;
	}
	// Verify the decypher
	if(strcmp(&buffer[size - 6], "c|-|3k") != 0)
	{
#if GLF_DEBUG
        Console::Println("Could not open the obfuscation map (%s), check that the file and key are good.", filename);
#endif
		return false;
	}

	char* bufferEnd = buffer + size - 6;
	*bufferEnd = 0;
	std::stringstream ss(buffer);
	int crc = 0;
	stringc scrambledFileName, realFileName;
	while(!ss.eof())
	{
		ss >> scrambledFileName >> realFileName >> crc;
		ObfuscationMap[realFileName] = scrambledFileName;
		glf::CrcChecker::SetFileEntry(realFileName.c_str(), (glf::uint)crc, false);
	}
	
	return true;
}

bool Fs::EmptyDir(const char* dir, openflag flag) {
	std::vector<char> buffer(2048);
	flag = ResolvePath(dir, flag, &buffer[0], buffer.size());

	return m.EmptyDir(&buffer[0], flag);
}

bool Fs::MkDir(const char* dir, openflag flag) {
	std::vector<char> buffer(2048);
	flag = ResolvePath(dir, flag, &buffer[0], buffer.size());

	return m.MkDir(&buffer[0], flag);
}

bool Fs::RmDir(const char* dir, openflag flag)
{
	std::vector<char> buffer(2048);
	flag = ResolvePath(dir, flag, &buffer[0], buffer.size());

	return m.RmDir(&buffer[0], flag);
}

bool Fs::RemoveFile(const char* path) {
	return m.RemoveFile(path);
}

bool Fs::DoesFileExist(const char* path) {
	return DoesFileExist(path, ios::loc_data);
}

bool Fs::DoesFileExist(const char* path, openflag flag) {
	std::vector<char> buffer(2048);
	flag = ResolvePath(path, flag, &buffer[0], buffer.size());	
	if(ArchiveManager::GetInstance()->IsUsingArchive(flag)) {
		bool isInArchive = ArchiveManager::GetInstance()->DoesFileExist(&buffer[0]);
		if(isInArchive) { 
			return true; 
		}
	}
	return m.DoesFileExist(&buffer[0]);
}

bool Fs::MountDrive(const char* name, const char* dst, openflag flag) {

	if(mNumLinked >= GLF_ASIZEOF(mLinked)) {
		return false;
	}

	Link& mount = *mLinked[mNumLinked++];

	mount.name = name;
	mount.destination = dst;
	mount.flag = flag;

	return true;

}

ios::openflag Fs::ResolvePath(const char* inPath, openflag flag, char* newPath, size_t newPathLength) {
	newPath[0] = 0;

	const char* root = "";
	const char* path = inPath;
	stringc lowerPath;

	if( IsForcingLowercase() )
	{		
		lowerPath = path;
		std::transform(lowerPath.begin(), lowerPath.end(), lowerPath.begin(), tolower);
		path = lowerPath.c_str();
	}

	root = GetDir(flag);

	const char* savedPath = path;
	if(!ObfuscationMap.empty())
	{
		// Skip ./
		if(path[0] == '.' && (path[1] == '/' || path[1] == '\\'))
		{
			path += 2;
		}
		// Skip the current working directory in the path
        unsigned int cwdlen = glf::Strlen(root);
		if(cwdlen > 0 && !glf::Strncmp(path, root, cwdlen))
		{
			path += cwdlen + 1;
		}
		std::map<stringc, stringc>::iterator iter = ObfuscationMap.find(path);
		if(iter != ObfuscationMap.end())
		{
			path = iter->second.c_str();
            flag |= ios::obfuscated | ios::check_crc;
		}
		else
		{
			path = savedPath;
		}
	}
	
	//resolve path of the type /<mountName>/
	if(path[1] == ':' //as in d:/projects
        || strncmp(path,root,strlen(root)) == 0) { // path is already prefixed with rootpath
		VJoinPath(newPath, newPathLength, 1, path);
		return flag;
	} else if(path[0] == '/') {
		const char* secondSlash = strchr(path+1, '/');
		if(secondSlash) {
			const char* restOfPath = secondSlash + 1;
			const char* mountName = path + 1;
			size_t mountNameLength = secondSlash - mountName;

			for(uint i = 0; i < mNumLinked; i++) {
				Link& mount = *mLinked[i];
				if(Strncmp(mountName, mount.name.c_str(), mountNameLength) == 0) { //case sensitive
					VJoinPath(newPath, newPathLength, 3, root, mount.destination.c_str(), restOfPath);
					
					flag |= mount.flag;
					return flag;
				}
			}
		}
#ifndef SN_TARGET_PSP2
		// A path on PSP2 is in the form of host0:./folder/file
		//
		// A path starting with / cannot be considered an absolute path
		// so we skip this part and let it fallthru; the root part
		// will be prepended to the newPath.
		VJoinPath(newPath, newPathLength, 1, path);
		return flag;
#endif
    }

	VJoinPath(newPath, newPathLength, 2, root, path);
	return flag;
}

bool Fs::UnmountDrive(const char* name) {
	if(mNumLinked == 0) {
		return false;
	}

	for(uint i = 0; i < mNumLinked; i++) {
		if(mLinked[i]->name == name) {
			mLinked[i] = mLinked[--mNumLinked];
			break;
		}
	}
	
	return true;

}

const char* Fs::GetMountedDrive(const char* name) {
	for(uint i = 0; i < mNumLinked; i++) {
		if(mLinked[i]->name == name) {
			return mLinked[i]->destination.c_str();
		}
	}
	return 0;
}


bool StartsWith(const char* str, const char* match) {
	return strstr(str, match) == str;
}

const char* FindFirstDifferentOf(const char* str, char c) {
	while(*str == c) {
		str++;
	}
	return str;
}
size_t StrLenUntil(const char* str, char c) {
	size_t len = 0;
	while(*str && *str != c) {
		++str;
		++len;
	}
	return len;
}

struct Str {
	const char* str;
	size_t len;
};

size_t NormalizePath(char* dst, size_t dstLen, const char* src) {
	GLFi_ASSERT(src != dst);
	if(dst) {
		//check that src is not in between dst and dstLen
		GLFi_ASSERT(src < dst || src >= dst + dstLen);
	}

	if(*src == 0) {
		if(dst) {
			Strcpy_s(dst, dstLen, ".");
		}
		return 2;
	}

	size_t initial_slashes = StartsWith(src, "/") ? 1 : 0;

	if(initial_slashes && StartsWith(src, "//") && !StartsWith(src, "///")) {
		initial_slashes = 2;
	}

	const char* it = src;
	fixed_vector<Str, 32> comps;
	while(it) {
		if(it == FindFirstDifferentOf(it, '/')) {
			Str s = { it, StrLenUntil(it, '/') };
			comps.push_back(s);
		}
		it = strchr(it, '/');

		if(it) 
			it++;
	}

	fixed_vector<Str, 32> new_comps;
	
	for(size_t i = 0; i < comps.size(); ++i) {
		size_t n = comps[i].len;
		const char* comp = comps[i].str;

		if(*comp == 0 || strncmp(comp, ".", n) == 0) {
			continue;
		}

		bool cond1 = strncmp(comp, "..", n) != 0;
		bool cond2 = (initial_slashes == 0 && new_comps.empty());
		bool cond3 = (!new_comps.empty() && strncmp(new_comps.back().str, "..", new_comps.back().len)==0);
		
		if(cond1 || cond2 || cond3) {
			new_comps.push_back(comps[i]);
		} else if(!new_comps.empty()) {
			new_comps.pop_back();
		}
	}

	char * path = dst;
	
	if(dst) {
		for(size_t i = 0; i < initial_slashes; ++i) {
			*path++ = '/';
		}
		*path = 0;
	}
	
	size_t written = initial_slashes;
	
	for(size_t i = 0; i < new_comps.size(); ++i) {
		if(i > 0) {
			if(dst) {
				*path++ = '/';
			} else {
				++written;
			}
		}
		if(dst) {
			strncpy(path, new_comps[i].str, new_comps[i].len);
			path += new_comps[i].len;
		} 
		written += new_comps[i].len;
	}
	
	*path = 0;

	return written + 1;
}

void VJoinPath(char* dst, size_t dstLen, size_t numToJoin, va_list vlist) {
	dst[0] = 0;

	char* end = dst;

	for(size_t i = 0; i < numToJoin; ++i) {
		const char* str = va_arg(vlist, const char*);

		if(str == 0) {
			continue;
		}
		
		size_t len = glf::Strlen(str);

		if(len == 0) {
			continue;
		}

		//const char* strend = str + len - 1; // never referenced

		if(i > 0) {
			if(end[-1] == '/') {
				if(*str == '/') {
					++str;
				} else {
				}
			} else {
				if(*str == '/') {
				} else {
					*end++ = '/';
				}
			}
		}

		glf::Strcpy(end, str);

		end += len;

		if(i+1 == numToJoin) {
			if(end[-1] == '/') {
				--end;
				*end = 0;
			}
		}

	}
}

void VJoinPath(char* dst, size_t dstLen, size_t numToJoin, ...) {
	va_list vlist;
	
	va_start(vlist, numToJoin);

	VJoinPath(dst, dstLen, numToJoin, vlist);

	va_end(vlist);
}

void AppendPath(stringc& src, const stringc& path0) {
	if(src.empty()) {
		src = path0;
	} else {
		char lastChar = src[src.size() - 1];
		if(lastChar == '/' || lastChar == '\\') {
			src += path0;
		} else {
			src.append("/");
			src.append(path0);
		}
	}
}
void AppendPath(stringc& src, const stringc& path0, const stringc& path1) {
	AppendPath(src, path0);
	AppendPath(src, path1);
}
void AppendPath(stringc& src, const stringc& path0, const stringc& path1, const stringc& path2) {
	AppendPath(src, path0);
	AppendPath(src, path1);
	AppendPath(src, path2);
}

stringc JoinPath(const stringc& path0) {
	stringc path;
	AppendPath(path, path0);
	return path;
}

stringc JoinPath(const stringc& path0, const stringc& path1) {
	stringc path;
	AppendPath(path, path0, path1);
	return path;
}

stringc JoinPath(const stringc& path0, const stringc& path1, const stringc& path2) {
	stringc path;
	AppendPath(path, path0, path1, path2);
	return path;
}

namespace fs {

DirHandle::DirHandle() :
	m(GLF_NEW_IMPL(*this)) {
	mLastError = ERROR_NONE;
	GLF_CHECK_IMPL_SIZE();
}

DirHandle::DirHandle(const char* path, openflag flag) :
	m(GLF_NEW_IMPL(*this)) {
	
	mLastError = ERROR_NONE;
	FindFirst(path, flag);
}

DirHandle::DirHandle(const DirHandle& handle, const char* path, openflag flag) :
	m(GLF_NEW_IMPL(*this)) {
	
	mLastError = ERROR_NONE;
	FindFirst(handle, path, flag);
}

DirHandle::~DirHandle() {
	Close();
	GLF_DELETE_IMPL();
}

bool DirHandle::FindFirst(const char* path, openflag flag) {
	std::vector<char> buffer(2048);

	mFlag = fs::ResolvePath(path, flag, &buffer[0], buffer.size());

	mPath = path;

	if(m.FindFirst(&buffer[0], mFlag, fileInfo)) {
		return _Filter();
	}

	return false;
}

bool DirHandle::FindFirst(const DirHandle& handle, const char* path, openflag flag) {
	
	
	if(path)
		mPath = JoinPath(handle.mPath, path);
	else
		mPath = JoinPath(handle.mPath);

	std::vector<char> buffer(1024);
	mFlag = fs::ResolvePath(mPath.c_str(), flag, &buffer[0], buffer.size());

	if(m.FindFirst(&buffer[0], mFlag, fileInfo)) {
		return _Filter();
	}

	return false;
}

bool DirHandle::_Filter() {

	//mFlag = flag;

	//Sprintf_s(mPath, "%s/%s", handle.mPath, path);

	//if(m.FindFirst(mPath, flag, fileInfo)) {
	while (true) {
		if(	!IsValidDir(fileInfo.filename.c_str()) || 
			(fileInfo.isDir && TestFlag(mFlag, list_dir) == false) ||
			(!fileInfo.isDir && TestFlag(mFlag, list_file) == false) ||
			(fileInfo.isHidden && TestFlag(mFlag, list_hidden) == false)) {
			
			if(m.FindNext(fileInfo)) {
				continue;
			} else {
				break;
			}
		} 
		return true;
	}
	//}
	return false;
}

bool DirHandle::FindNext() {
	while(m.FindNext(fileInfo)) {

		//GLF_ASSERT(false);
		if(_Filter()) {
			return true;
		}
		//if(	!IsValidDir(fileInfo.filename) ||
		//	(fileInfo.isDir && TestFlag(mFlag, list_dir) == false) ||
		//	(!fileInfo.isDir && TestFlag(mFlag, list_file) == false) ||
		//	(fileInfo.isHidden && TestFlag(mFlag, list_hidden) == false)) {
		//	continue;
		//} 
		
	}
	return false;
}

void DirHandle::Close() {
	m.Close();
}

Error DirHandle::GetLastError() const {
	return mLastError;
}

ios::openflag DirHandle::GetFlag() const {
	return m.mFlag;
}

bool MkDir(const char* dir, ios::openflag flag) {
	return GetGlobals()->fileSystem.MkDir(dir, flag);
}

bool MountDrive(const char* drive, const char* dst, ios::openflag flag) {
	return GetGlobals()->fileSystem.MountDrive(drive, dst, flag);
}

ios::openflag ResolvePath(const char* path, ios::openflag flag, char* newPath, size_t newPathLength) {
	return GetGlobals()->fileSystem.ResolvePath(path, flag, newPath, newPathLength);
}

bool UnmountDrive(const char* drive) {
	return GetGlobals()->fileSystem.UnmountDrive(drive);
}

const char* GetMountedDrive(const char* name) {
	return GetGlobals()->fileSystem.GetMountedDrive(name);
}

const char* GetHomeDir() {
	return GetGlobals()->fileSystem.GetDir(ios::loc_home);
}

const char* GetTmpDir() {
	return GetGlobals()->fileSystem.GetDir(ios::loc_tmp);
}

const char* GetCurDir() {
	return GetGlobals()->fileSystem.GetDir(ios::loc_cwd);
}

bool RemoveFile(const char* path) {
	return GetGlobals()->fileSystem.RemoveFile(path);
}

bool DoesFileExist(const char* path) {
	return GetGlobals()->fileSystem.DoesFileExist(path);
}
bool DoesFileExist(const char* path, ios::openflag flag) {
	return GetGlobals()->fileSystem.DoesFileExist(path, flag);
}

char gAlternateDataDirs[8][64] = {
	".", "", "", "", "", "", "", "",
};

bool gIsUsingAlternateDataDirs = true;

void SetAlternateDataDir(size_t idx, const char* dir) {
	GLF_CHECK_RANGE(idx, gAlternateDataDirs);
	GLF_ASSERT(strlen(dir) < GLF_ASIZEOF(gAlternateDataDirs[0]));
	
	strcpy(gAlternateDataDirs[idx], dir);
}

const char* GetAlternateDataDir(size_t idx) {
	return gAlternateDataDirs[idx];
}

size_t GetAlternateDataDirCount() {
	//quick an easy way to make sure the feature is turned off...
	return (IsUsingAlternateDataDirs()) ? GLF_ASIZEOF(gAlternateDataDirs) : 0;
}

bool IsUsingAlternateDataDirs() {
	return gIsUsingAlternateDataDirs;
}

void SetIsUsingAlternateDataDirs( bool isEnabled ) {
	gIsUsingAlternateDataDirs = isEnabled;
}

#if GLF_COMPILER_SNC
#   pragma diag_pop
#endif

} // namespace fs
} // namespace glf
